package com.atguigu.gmall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.common.cache.GmallCache;
import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.product.controller.SkuManageController;
import com.atguigu.gmall.product.mapper.SkuAttrValueMapper;
import com.atguigu.gmall.product.mapper.SkuSaleAttrValueMapper;
import com.atguigu.gmall.product.model.*;
import com.atguigu.gmall.product.service.*;
import com.atguigu.gmall.rabbit.config.MqConst;
import com.atguigu.gmall.rabbit.service.RabbitService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author: atguigu
 * @create: 2023-07-26 10:50
 */
@Slf4j
@Service
public class SkuManageServiceImpl implements SkuManageService {

    @Autowired
    private SkuInfoService skuInfoService;

    @Autowired
    private SkuImageService skuImageService;

    @Autowired
    private SkuAttrValueService skuAttrValueService;

    @Autowired
    private SkuSaleAttrValueService skuSaleAttrValueService;


    @Autowired
    private RedisTemplate redisTemplate;


    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RabbitService rabbitService;


    /**
     * 保存商品SKU
     * 1.将sku商品基本信息封装为SkuInfo对象，存入sku_info表一条记录
     * 2.将选择商品多张sku图片封装SkuImage集合，批量存入sku_image表
     * 3.将选择多个平台属性以及值封装SkuAttrValue集合，批量存入sku_attr_value表
     * 4.将选择多个销售属性以及值封装SkuSaleAttrValue集合，批量存入sku_sale_attr_value表
     *
     * @param skuInfo
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)  //底层是AOP，AOP底层动态代理
    public void saveSkuInfo(SkuInfo skuInfo) {
        if (skuInfo.getId() == null) {
            //新增操作
            //1.将sku商品基本信息封装为SkuInfo对象，存入sku_info表一条记录
            skuInfoService.save(skuInfo);
            Long skuId = skuInfo.getId();
            //2.将选择商品多张sku图片封装SkuImage集合，批量存入sku_image表
            List<SkuImage> skuImageList = skuInfo.getSkuImageList();
            if (!CollectionUtils.isEmpty(skuImageList)) {
                skuImageList.stream().forEach(skuImage -> {
                    //关联Sku
                    skuImage.setSkuId(skuId);
                });
                //批量保存商品sku图片
                skuImageService.saveBatch(skuImageList);
            }

            //3.将选择多个平台属性以及值封装SkuAttrValue集合，批量存入sku_attr_value表
            List<SkuAttrValue> skuAttrValueList = skuInfo.getSkuAttrValueList();
            if (!CollectionUtils.isEmpty(skuAttrValueList)) {
                skuAttrValueList.stream().forEach(skuAttrValue -> {
                    //关联Sku
                    skuAttrValue.setSkuId(skuId);
                });
                //批量保存sku平台属性
                skuAttrValueService.saveBatch(skuAttrValueList);
            }

            //4.将选择多个销售属性以及值封装SkuSaleAttrValue集合，批量存入sku_sale_attr_value表
            List<SkuSaleAttrValue> skuSaleAttrValueList = skuInfo.getSkuSaleAttrValueList();
            if (!CollectionUtils.isEmpty(skuSaleAttrValueList)) {
                skuSaleAttrValueList.stream().forEach(skuSaleAttrValue -> {
                    //关联Spu
                    skuSaleAttrValue.setSpuId(skuInfo.getSpuId());
                    //关联Sku
                    skuSaleAttrValue.setSkuId(skuId);
                });
                //批量保存sku销售属性值
                skuSaleAttrValueService.saveBatch(skuSaleAttrValueList);
            }

            //将新增商品ID放入布隆过滤器
            RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
            bloomFilter.add(skuId);
        } else {
            //修改商品SKU
            //双写策略1：先修改数据库再改缓存  问题：并发双写场景中，由于线程执行顺序无法确定，导致数据不一致
            //1.修改商品SKU信息
            //2.将修改后SKU信息写回到缓存中

            //双写策略2：先修改缓存再改数据库  问题：并发双写场景中，由于线程执行顺序无法确定，导致数据不一致
            //1.将修改后SKU信息写回到缓存中
            //2.修改商品SKU信息

            //删除缓存策略：先删缓存再修改数据  问题：并发双写场景中，由于线程执行顺序无法确定，导致数据不一致
            //1.先删除缓存
            //////期间有其他查询线程将数据库中“旧”的数据写入缓存
            //2.更新数据库

            //延迟双删  前提：存在另外一些并发查询线程   结果：数据库更新，缓存为空  问题：时间不太好估算；主从复制-数据主从同步耗时不确定。存在数据不一致问题
            //1.先删除缓存
            //2.再修改数据库
            //////////////有可能存在其他的并发查询线程：将数据库中“旧”的数据写回到缓存中
            //3.睡眠一段时间 经过多次测试-获取查询DB放入缓存需要时间  保证并发读线程执行完毕
            //4.再删除缓存

            //读写锁方案

            //binglog方式
            //1.修改数据库  binglog日志产生      将来由另外程序读取到修改后数据-删除/更新缓存

        }
    }

    /**
     * 根据分类ID分页查询商品SKU列表
     *
     * @param skuInfoPage 分页对象
     * @param category3Id 分类ID
     * @return
     */
    @Override
    @Transactional(readOnly = true)
    public Page<SkuInfo> getSkuByPage(Page<SkuInfo> skuInfoPage, Long category3Id) {
        //1.条件
        LambdaQueryWrapper<SkuInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SkuInfo::getCategory3Id, category3Id);
        queryWrapper.orderByDesc(SkuInfo::getId, SkuInfo::getUpdateTime);
        //2.执行分页查询
        return skuInfoService.page(skuInfoPage, queryWrapper);
    }

    /**
     * 对商品SKU上架处理
     *
     * @param skuId
     * @return
     */
    @Override
    public void onSale(Long skuId) {
        SkuInfo skuInfo = new SkuInfo(skuId);
        skuInfo.setIsSale(1);
        skuInfoService.updateById(skuInfo);

        //1.将商品信息放入分布式缓存  2.将上架商品存入全文检索搜索引擎ElasticSearch
        //String key = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;
        //redisTemplate.opsForValue().set(key, skuInfo, RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);

        //同步ES文档数据 方案一：同步双写 方案二：同步Feign调用搜索服务接口
        //方案三：基于MQ消息队列实现同步
        rabbitService.sendMsg(MqConst.EXCHANGE_DIRECT_GOODS, MqConst.ROUTING_GOODS_UPPER, skuId);
    }

    /**
     * 对商品SKU下架处理
     *
     * @param skuId
     * @return
     */
    @Override
    public void cancelSale(Long skuId) {
        try {
            //1.清理分布式缓存或者修改  TODO  2.将下架商品从全文检索搜索引擎ElasticSearch删除
            //采用延时双删方案
            //①先删除缓存
            String key = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;
            redisTemplate.delete(key);
            //②修改数据库
            //SQL : update sku_info set is_sale = 0 where id = ?
            //1.创建用于更新Wrapper
            LambdaUpdateWrapper<SkuInfo> updateWrapper = new LambdaUpdateWrapper<>();
            updateWrapper.set(SkuInfo::getIsSale, 0);
            updateWrapper.eq(SkuInfo::getId, skuId);
            skuInfoService.update(updateWrapper);
            //③睡眠一段时间（查询数据写入缓存耗时）避免存在并发读情况
            TimeUnit.MILLISECONDS.sleep(500);
            //④再删除一次缓存
            redisTemplate.delete(key);

            //3.同步ES索引库
            rabbitService.sendMsg(MqConst.EXCHANGE_DIRECT_GOODS, MqConst.ROUTING_GOODS_LOWER, skuId);
        } catch (Exception e) {
            log.error("【商品服务】商品下架异常：{}", e);
            throw new RuntimeException(e);
        }
    }


    /**
     * 采用Redisson框架实现分布式锁，避免查询商品信息出现缓存击穿问题
     *
     * @param skuId
     * @return
     */
    @Override
    public SkuInfo getSkuInfo(Long skuId) {
        try {
            //1.尝试从缓存中获取数据 命中缓存：直接响应  未命中：尝试获取分布式锁
            //1.1 构建业务数据Key 形式  前缀:数据标识:后缀   sku:24:info
            String dataKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;
            //1.2 从缓存中获取业务数据 有数据：直接响应  没有：执行第2步
            SkuInfo skuInfo = (SkuInfo) redisTemplate.opsForValue().get(dataKey);
            if (skuInfo != null) {
                return skuInfo;
            }
            //2.尝试获取分布式锁(解决缓存击穿问题) 获取成功：执行回源查询数据库  未获取成功：睡眠自旋
            //2.1 构建业务数据对应锁的key 前缀：数据标识：后缀 sku:24:lock
            String lockKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKULOCK_SUFFIX;
            //2.2 创建分布式锁对象
            RLock lock = redissonClient.getLock(lockKey);

            //2.3 尝试获取分布式锁
            //lock.lock(); //底层会产生“看门狗”对key进行续期
            //如果要调用tryLock方法，锁的释放时间必须设置“-1”才会启动看门狗定时器
            boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, -1, TimeUnit.SECONDS);
            if (flag) {
                try {
                    // 获取锁成功
                    //2.3.1 避免处于获取锁等待中线程，再次获取锁成功 继续查询多次数据库，解决：获取锁后再查一遍缓存
                    skuInfo = (SkuInfo) redisTemplate.opsForValue().get(dataKey);
                    if (skuInfo != null) {
                        return skuInfo;
                    }
                    //2.3.2 执行查询数据库（回源）
                    skuInfo = this.getSkuInfoFromDB(skuId);
                    if (skuInfo == null) {
                        //2.3.2.1 查询数据库为空 将空结果缓存到Redis 设置10分钟有效时间
                        redisTemplate.opsForValue().set(dataKey, skuInfo, RedisConst.SKUKEY_TEMPORARY_TIMEOUT, TimeUnit.SECONDS);
                        return skuInfo;
                    } else {
                        //2.3.2.2 查询数据库业务结果缓存到Redis 设置24小时有效时间
                        redisTemplate.opsForValue().set(dataKey, skuInfo, RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
                        return skuInfo;
                    }
                } finally {
                    //2.4 业务执行完毕后将锁释放
                    lock.unlock();
                }
            } else {
                //2.3.2 获取锁失败：睡眠自旋
                Thread.sleep(500);
                return this.getSkuInfo(skuId);
            }
        } catch (Exception e) {
            log.error("[商品服务]查询商品信息异常：{}", e);
            // 3. 兜底处理方案：查询数据库
            return this.getSkuInfoFromDB(skuId);
        }
    }

    /**
     * 使用SpringDataRedis实现分布式锁：底层使用命令：set key val nx ex
     * 根据商品SKUID查询商品基本信息以及SKU相关图片列表
     *
     * @param skuId
     * @return
     */
   /* @Override
    public SkuInfo getSkuInfo(Long skuId) {
        //1.优先从分布式缓存Redis中获取数据  命中缓存：直接返回  未命中：尝试获取分布式锁
        //1.1 构建商品信息业务数据Key形式  sku:skuId:info
        String dataKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;  //todo 魔法字符串-严禁出现

        //1.2 从Redis中获取商品信息
        SkuInfo skuInfo = (SkuInfo) redisTemplate.opsForValue().get(dataKey);
        if (skuInfo != null) {
            //将命中缓存数据返回
            return skuInfo;
        }
        //2.尝试获取分布式锁  获取锁成功：查询数据库（执行数据回源操作）  获取锁失败：睡眠后自旋
        //2.1 构建商品信息锁的key形式 sku:skuId:lock 锁粒度小些  针对于当前业务数据标识 生成锁
        String lockKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKULOCK_SUFFIX;
        //2.2 生成当前线程锁的值
        String lockVal = UUID.randomUUID().toString();

        //2.2 采用set nx ex 命令实现获取分布式锁
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockKey, lockVal, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
        if (flag) {
            try {
                //2.3 获取锁成功，则执行查询数据库业务（一定要记得将锁释放）
                skuInfo = this.getSkuInfoFromDB(skuId);
                //2.3.1 判断查库后业务数据  存在：  不存在：情况
                if (skuInfo == null) {
                    //不存在 将空的数据缓存短暂时间
                    redisTemplate.opsForValue().set(dataKey, null, RedisConst.SKUKEY_TEMPORARY_TIMEOUT, TimeUnit.SECONDS);
                    return skuInfo;
                } else {
                    //存在 将查询到数据放入Redis缓存，且返回
                    redisTemplate.opsForValue().set(dataKey, skuInfo, RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
                    return skuInfo;
                }
            } finally {
                //2.3.2 将锁释放 采用lua脚本
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
                redisScript.setScriptText(script);
                redisScript.setResultType(Long.class);
                redisTemplate.execute(redisScript, Arrays.asList(lockKey), lockVal);
            }
        } else {
            try {
                //2.4 获取锁失败，睡眠后再自旋
                Thread.sleep(500);
                return this.getSkuInfo(skuId);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }*/


    /**
     * 从数据库中获取：根据商品SKUID查询商品基本信息以及SKU相关图片列表
     *
     * @param skuId
     * @return
     */
    @Override
    @GmallCache(prefix = "sku:", suffix = ":info") //对当前方法 环绕通知   缓存+分布式锁（通知）   执行目标方法   释放锁（通知）
    public SkuInfo getSkuInfoFromDB(Long skuId) {
        //1.根据skuId主键查询商品SKU信息
        SkuInfo skuInfo = skuInfoService.getById(skuId);
        //2.根据SkuID查询商品图片列表
        if (skuInfo != null) {
            LambdaQueryWrapper<SkuImage> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(SkuImage::getSkuId, skuId);
            List<SkuImage> skuImageList = skuImageService.list(queryWrapper);
            skuInfo.setSkuImageList(skuImageList);
        }
        return skuInfo;
    }

    /**
     * 根据商品SkuID查询商品实时价格
     * 保护数据库，再执行查询价格前必选先获取到分布式锁
     *
     * @param skuId
     * @return
     */
    @Override
    public BigDecimal getSkuPrice(Long skuId) {
        try {
            //1.尝试先获取分布式锁
            String lockKey = RedisConst.SKUKEY_PREFIX + "price:" + skuId + RedisConst.SKULOCK_SUFFIX;
            RLock lock = redissonClient.getLock(lockKey);
            boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, -1, TimeUnit.SECONDS);
            if (flag) {
                try {
                    //2.获取锁成功后，业务：才能执行查询数据库逻辑
                    LambdaQueryWrapper<SkuInfo> queryWrapper = new LambdaQueryWrapper<>();
                    queryWrapper.eq(SkuInfo::getId, skuId);
                    queryWrapper.select(SkuInfo::getPrice);
                    SkuInfo skuInfo = skuInfoService.getOne(queryWrapper);
                    if (skuInfo != null) {
                        return skuInfo.getPrice();
                    }
                } finally {
                    //3.将锁释放
                    lock.unlock();
                }
            } else {
                //获取锁失败
                TimeUnit.MILLISECONDS.sleep(500);
                return this.getSkuPrice(skuId);
            }
        } catch (Exception e) {
            log.error("[商品服务]获取商品价格异常：{}", e);
            throw new RuntimeException(e);
        }
        return new BigDecimal("0.0");
    }


    /**
     * 根据商品SkuID查询sku商品所有属性名称以及属性值
     *
     * @param skuId
     * @return
     */
    @Override
    @GmallCache(prefix = "attrs:", suffix = ":info")
    public List<BaseAttrInfo> getAttrList(Long skuId) {
        SkuAttrValueMapper skuAttrValueBaseMapper = (SkuAttrValueMapper) skuAttrValueService.getBaseMapper();
        return skuAttrValueBaseMapper.getAttrList(skuId);
    }


    /**
     * 通过spu下的一组销售属性切换sku商品页面
     *
     * @param spuId
     * @return {"3732|3734":"24", "3732|3735":"25", "3733|3734":"26", "3733|3735":"27"}
     */
    @Override
    @GmallCache(prefix = "skuSaleAttrValue:")
    public String getSkuValueIdsMap(Long spuId) {
        SkuSaleAttrValueMapper skuSaleAttrValueMapper = (SkuSaleAttrValueMapper) skuSaleAttrValueService.getBaseMapper();
        //查询到当前商品切换sku列表
        List<Map> list = skuSaleAttrValueMapper.getSkuValueIdsMap(spuId);
        //遍历集合按照要求封装json字符串
        if (!CollectionUtils.isEmpty(list)) {
            Map<Object, Object> mapResult = new HashMap<>();
            list.forEach(map -> {
                Object value_ids = map.get("value_ids"); //"3732|3734"
                Object sku_id = map.get("sku_id"); // 24
                mapResult.put(value_ids, sku_id);
            });
            return JSON.toJSONString(mapResult);
        }
        return null;
    }
}
